using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using Microsoft.Xna.Framework.Content.Pipeline.Processors;
using XNOgre.Animation;

namespace XNOgreCustomContent
{
    /// <summary>
    /// This class will be instantiated by the XNA Framework Content Pipeline
    /// to apply custom processing to content data, converting an object of
    /// type TInput to TOutput. The input and output types may be the same if
    /// the processor wishes to alter data without changing its type.
    ///
    /// This should be part of a Content Pipeline Extension Library project.
    ///
    /// TODO: change the ContentProcessor attribute to specify the correct
    /// display name for this processor.
    /// </summary>
    [ContentProcessor(DisplayName = "Animated Model - XNOgre")]
    public class SkinnedModelProcessor : ModelProcessor
    {
        public override ModelContent Process(NodeContent input, ContentProcessorContext context)
        {
            ValidateMesh(input, context, null);
            var skeleton = MeshHelper.FindSkeleton(input);
            if (skeleton == null)
                throw new InvalidOperationException("Input skeleton not found");
           
            FlattenTransforms(input, skeleton);
            var bones = MeshHelper.FlattenSkeleton(skeleton);
            List<Matrix> bindPose = new List<Matrix>();
            List<Matrix> inverseBindPose = new List<Matrix>();
            List<int> skeletonHiearchy = new List<int>();

            foreach (var bone in bones)
            {
                bindPose.Add(bone.Transform);
                inverseBindPose.Add(Matrix.Invert(bone.AbsoluteTransform));
                skeletonHiearchy.Add(bones.IndexOf((bone.Parent as BoneContent)));
            }

            List<Animation> animations = ProcessAnimations(skeleton.Animations, bones);
            Skeleton mySkeleton = new Skeleton(animations, bindPose, inverseBindPose, skeletonHiearchy);
           
            ModelContent model = base.Process(input, context);
            model.Tag = mySkeleton;
            return model;
        }

        private List<Animation> ProcessAnimations(AnimationContentDictionary animations, IList<BoneContent> bones)
        {
            Dictionary<string, int> boneMap = new Dictionary<string, int>();
            for (int i = 0; i < bones.Count; i++)
            {
                string boneName = bones[i].Name;
                if (!String.IsNullOrEmpty(boneName))
                {
                    boneMap.Add(boneName, i);
                }
            }

            List<Animation> animStates = new List<Animation>();
            foreach (var key in animations.Keys)
            {
                Animation animState = ProcessAnimation(animations[key], boneMap);
                animStates.Add(animState);
            }
            if (animStates.Count == 0)
            {
                throw new InvalidContentException("Input file does not contain any animations");
            }
            
            return animStates;

        }
        static Animation ProcessAnimation(AnimationContent animation, Dictionary<string, int> boneMap)
        {
            List<Keyframe> keyframes = new List<Keyframe>();
            foreach (var key in animation.Channels.Keys)
            {
                int boneIndex;
                if (!boneMap.TryGetValue(key, out boneIndex))
                {
                    throw new InvalidContentException("Found animtion for bone which is not part of the skeleton.");
                }

                foreach (var keyframe in animation.Channels[key])
                {
                    keyframes.Add(new Keyframe(boneIndex, keyframe.Time, keyframe.Transform));
                }

            }


            keyframes.Sort(CompareKeyFrameTimes);
            if (keyframes.Count == 0)
            {
                throw new InvalidContentException("Animation has no keyframes");
            }
            if (animation.Duration <= TimeSpan.Zero)
            {
                throw new InvalidContentException("Animation has a zero duration.");
            }

            Animation anim = new Animation(animation.Name, animation.Duration, keyframes);
            return anim;
        }
        static int CompareKeyFrameTimes(Keyframe a, Keyframe b)
        {
            return a.Time.CompareTo(b.Time);
        }
        static void FlattenTransforms(NodeContent node, BoneContent skeleton)
        {
            foreach (var child in node.Children)
            {
                if (child == skeleton)
                    continue;

                MeshHelper.TransformScene(child, child.Transform);
                child.Transform = Matrix.Identity;
                FlattenTransforms(child, skeleton);
            }
        }
        static bool MeshHasSkinning(MeshContent mesh)
        {
            foreach (var geometry in mesh.Geometry)
            {
                if (!geometry.Vertices.Channels.Contains(VertexChannelNames.Weights()))
                    return false;
            }
            return true;
        }
        static void ValidateMesh(NodeContent node, ContentProcessorContext context, string parentBoneName)
        {
            MeshContent mesh = node as MeshContent;

            if (mesh != null)
            {
                if (parentBoneName != null)
                {
                    context.Logger.LogWarning(null, null, "Mesh is a child of bone {1}. Can't correctly handle.");
                }
                if (!MeshHasSkinning(mesh))
                {
                    context.Logger.LogWarning(null, null,"Mesh doesn't have skinning nifo");
                    return;
                }
            }
            else if (node is BoneContent)
            {
                parentBoneName = node.Name;
            }

            foreach (var child in node.Children)
            {
                ValidateMesh(child, context, parentBoneName);
            }
        }
    }
}